package org.ww.product.web;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ww.common.utils.redis.RedisCache;
import org.ww.product.entity.CategoryEntity;
import org.ww.product.service.CategoryService;
import org.ww.product.vo.CatelogLevel2Vo;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @program: gmall
 * @description: indexController
 * @author: ww
 * @create: 2024-02-20 11:23
 **/
@Slf4j
@Controller
public class IndexController {

    private static final String CATELOG_CACHE = "catelog";

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping({"/","index.html"})
    public String indexPage(Model model){
        //查询所有的一级分类
        List<CategoryEntity> categoryEntityList = categoryService.getLevel1Category();

        model.addAttribute("categorys",categoryEntityList);
        return "index";
    }

    //index/catalog.json
//    @ResponseBody
//    @GetMapping("/index/catalog.json")
//    public Map<String,List<CatelogLevel2Vo>> getCatalogJson(){
////        long startTime = System.currentTimeMillis();
////        log.info("第一阶段耗时：{}",System.currentTimeMillis() - startTime);
//        /**
//         * 1、空结果缓存/使用布隆过滤器：解决缓存穿透问题
//         * 2、设置过期时间（加随机值）：解决缓存雪崩问题
//         * 3、加分布式锁：解决缓存击穿问题
//         * 加锁如果用本地锁的话只能锁住当前线程：Sychronized(this)
//         * 所以这块我们用分布式锁（缺点：性能有一定缺陷）
//         */
//        //加入缓存逻辑
//        Map<String,List<CatelogLevel2Vo>>  map = redisCache.getCacheObject(CATELOG_CACHE);
////        String s = redisTemplate.opsForValue().get(CATELOG_CACHE);
////        Map<String, List<CatelogLevel2Vo>> map = JSON.parseObject(s, new TypeReference<Map<String, List<CatelogLevel2Vo>>>() {
////        });
//        log.info("缓存命中......");
//        if(map == null){
//            log.info("缓存未命中......");
//            //调用同步方法
//            map = this.getCatalogJsonJuc();
//        }
//
////        log.info("业务处理时间:{}",System.currentTimeMillis() - startTime);
//        return map;
//    }


    /**
     * 缓存一致性的解决方案：都要满足缓存数据的最终一致性
     * 双写模式：可能导致缓存脏数据问题
     * 两个线程同时进行更改数据库，1号线程先改完库，然后2号也改完了库，但是2号在1号之前修改了缓存，1号最后修改缓存，导致缓存和数据库不一致问题
     *  解决双写脏数据问题：、
     *      1、加锁，只有1号线更新完之后在让其他线程进行更改
     *      2、暂时性脏数据问题，缓存过期以后，又能得到最新的正确数据
     * 失效模式：也会导致脏数据的问题：同样可以使用通过加锁的形式进行解决
     *
     * 对于每个缓存我们都需要加上过期时间
     * 对于并发几率比较小的脏数据，只要能容忍一定程度的数据不一致，我们就无需就行处理这些脏数据，等缓存过期之后下次查询就会自动进行更新
     *
     * 对于经常需要使用的数据，并且对数据正确性要求性比较高的，无需进行缓存，应直接实时的进行查询数据库
     *
     * @return
     */

    /**
     * 我们系统的缓存一致性解决方案：
     * 1、每个缓存都设置缓存过期时间，数据过期下一次查询触发主动更新
     * 2、读写数据的时候，加上分布式的读写锁。
     *      经常写，经常读
     * @return
     */
    @ResponseBody
    @GetMapping("/index/catalog.json")
    public Map<String,List<CatelogLevel2Vo>> getCatalogJson(){
        /**
         * 1、空结果缓存/使用布隆过滤器：解决缓存穿透问题
         * 2、设置过期时间（加随机值）：解决缓存雪崩问题
         * 3、加分布式锁：解决缓存击穿问题
         * 加锁如果用本地锁的话只能锁住当前线程：Sychronized(this)
         * 所以这块我们用分布式锁（缺点：性能有一定缺陷）
         *
         * 这块为什么将锁加载controller层，能不能加在service层，答案是不能的：
         * 因为service层采用了缓存注解@Cacheable，如果使用在serivce层，则数据查询完之后还没来得及存入缓存，此时锁就会被释放掉了，
         * 然后下一个线程就会接着进入到程序中进行执行sql
         */
        RLock lock = redissonClient.getLock("catalogJson-lock");
        Map<String, List<CatelogLevel2Vo>> map = new HashMap<>();
        try{
            lock.lock();
            map = categoryService.getCatalogJson();
        }finally {
            lock.unlock();
        }
        return map;
    }

    private synchronized Map<String, List<CatelogLevel2Vo>> getCatalogJsonJuc() {
        //二次缓存获取/二次判断
        Map<String,List<CatelogLevel2Vo>> map = redisCache.getCacheObject(CATELOG_CACHE);
//        String s = redisTemplate.opsForValue().get(CATELOG_CACHE);
//        Map<String, List<CatelogLevel2Vo>> map = JSON.parseObject(s, new TypeReference<Map<String, List<CatelogLevel2Vo>>>() {
//        });
        if(map == null){
            log.info("缓存未命中...查询数据库...");
            List<CategoryEntity> allCategory = categoryService.getAllCategory();

            //构造分类数据
            map = Optional.ofNullable(allCategory)
                    .orElseGet(ArrayList::new)
                    .stream()
                    .collect(Collectors.toMap(l1 -> l1.getCatId().toString(), l1 -> {
                        //构造该一级分类的所有二级分类
                        List<CatelogLevel2Vo> catelogLevel2Vos = allCategory.stream()
                                .filter(item -> item.getParentCid().equals(l1.getCatId()))
                                .map(item -> {
                                    CatelogLevel2Vo catelogLevel2Vo = new CatelogLevel2Vo();
                                    catelogLevel2Vo.setCatalog1Id(item.getParentCid().toString());
                                    catelogLevel2Vo.setName(item.getName());
                                    catelogLevel2Vo.setId(item.getCatId().toString());

                                    //构造三级分类
                                    List<CatelogLevel2Vo.Catelog3Vo> catelog3Vos = allCategory.stream()
                                            .filter(l3 -> l3.getParentCid().equals(item.getCatId()))
                                            .map(l3 -> {
                                                CatelogLevel2Vo.Catelog3Vo catelog3Vo = new CatelogLevel2Vo.Catelog3Vo();
                                                catelog3Vo.setCatalog2Id(l3.getParentCid().toString());
                                                catelog3Vo.setName(l3.getName());
                                                catelog3Vo.setId(l3.getCatId().toString());
                                                return catelog3Vo;
                                            }).collect(Collectors.toList());
                                    catelogLevel2Vo.setCatalog3List(catelog3Vos);
                                    return catelogLevel2Vo;
                                }).collect(Collectors.toList());

                        return catelogLevel2Vos;
                    }));
            redisCache.setCacheObject(CATELOG_CACHE,map);
//            redisTemplate.opsForValue().set(CATELOG_CACHE, JSON.toJSONString(map));
        }
        return map;
    }


    private Map<String, List<CatelogLevel2Vo>> getCatalogJsonFromDb() {
        //二次缓存获取/二次判断
        Map<String,List<CatelogLevel2Vo>>  map = redisCache.getCacheObject(CATELOG_CACHE);
        if(map == null){
            log.info("缓存未命中...查询数据库...");
            List<CategoryEntity> allCategory = categoryService.getAllCategory();

            //构造分类数据
            map = Optional.ofNullable(allCategory)
                    .orElseGet(ArrayList::new)
                    .stream()
                    .collect(Collectors.toMap(l1 -> l1.getCatId().toString(), l1 -> {
                        //构造该一级分类的所有二级分类
                        List<CatelogLevel2Vo> catelogLevel2Vos = allCategory.stream()
                                .filter(item -> item.getParentCid().equals(l1.getCatId()))
                                .map(item -> {
                                    CatelogLevel2Vo catelogLevel2Vo = new CatelogLevel2Vo();
                                    catelogLevel2Vo.setCatalog1Id(item.getParentCid().toString());
                                    catelogLevel2Vo.setName(item.getName());
                                    catelogLevel2Vo.setId(item.getCatId().toString());

                                    //构造三级分类
                                    List<CatelogLevel2Vo.Catelog3Vo> catelog3Vos = allCategory.stream()
                                            .filter(l3 -> l3.getParentCid().equals(item.getCatId()))
                                            .map(l3 -> {
                                                CatelogLevel2Vo.Catelog3Vo catelog3Vo = new CatelogLevel2Vo.Catelog3Vo();
                                                catelog3Vo.setCatalog2Id(l3.getParentCid().toString());
                                                catelog3Vo.setName(l3.getName());
                                                catelog3Vo.setId(l3.getCatId().toString());
                                                return catelog3Vo;
                                            }).collect(Collectors.toList());
                                    catelogLevel2Vo.setCatalog3List(catelog3Vos);
                                    return catelogLevel2Vo;
                                }).collect(Collectors.toList());

                        return catelogLevel2Vos;
                    }));
            redisCache.setCacheObject(CATELOG_CACHE,map);
        }
        return map;
    }


    /**
     * lock.lock() ： 默认加锁30s，可以自己进行手动解锁，但是设置的业务时间必须大于业务时间，否则锁时间到了之后，不会进行自动续期
     * 1、有个看门狗机制：在业务执行期间会帮助我们自动续期
     * 2、为了防止死锁，业务宕机的时候，其他线程还可以再看门狗时间失效后进行占锁
     *
     * 最佳实战：
     * 使用自定义的过期时间  lock.lock(30, TimeUnit.SECONDS)，省掉了整个续期的操作，手动解锁
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        RLock lock = redissonClient.getLock("my-lock");

        try{
            //1 上锁
            lock.lock();
            log.info("上锁成功，开始执行业务代码:"+Thread.currentThread().getId());
            Thread.sleep(30000);
        }catch (Exception e){

        }finally {
            log.info("释放锁:"+Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";
    }


    /**
     * 保证一定能够读到最新数据，修改期间，写锁是一个排他所（互斥锁）。读锁是一个共享锁
     * 写锁没释放就必须等待
     * 读 + 读 ： 相当于无锁，在redis里边只会记载哪些读锁
     * 读 + 写 ： 读锁没释放之前，写锁只能等待
     * 写 + 写 ： 阻塞等待
     * 写 + 读 ： 读锁等待写锁释放
     * 总结：只要有写锁的存在，都必须要进行等待
     * @return
     */
    @ResponseBody
    @GetMapping("/write")
    public String writeLock(){
        String s = UUID.randomUUID().toString();
        RReadWriteLock writeReadLock = redissonClient.getReadWriteLock("writeReadLock");
        RLock wLock = writeReadLock.writeLock();
        try {
            wLock.lock();
            log.info("上写锁，进行写业务");
            redisCache.setCacheObject("writeReadLock",s);
            Thread.sleep(30000);
        }catch (Exception e){

        } finally {
            log.info("释放写锁:"+Thread.currentThread().getId());
            wLock.unlock();
        }
        return s;
    }

    @ResponseBody
    @GetMapping("/read")
    public String readLock(){
        RReadWriteLock writeReadLock = redissonClient.getReadWriteLock("writeReadLock");
        RLock rLock = writeReadLock.readLock();
        String cacheObject = "";
        try {
            rLock.lock();
            log.info("上读锁，进行读业务");
            cacheObject = redisCache.getCacheObject("writeReadLock").toString();
        }catch (Exception e){

        } finally {
            log.info("释放读锁:"+Thread.currentThread().getId());
            rLock.unlock();
        }
        return cacheObject;
    }


    /**
     * 停车或者限流
     * @return
     */
    @GetMapping("park")
    @ResponseBody
    public String park(){
        RSemaphore park = redissonClient.getSemaphore("park");
        boolean b = park.tryAcquire();  //尝试获取一个位置
        if(b){
            return "获取车位成功";
        }else{
            return "获取车位失败";
        }
    }

    @GetMapping("go")
    @ResponseBody
    public String go(){
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();  //释放一个车位
        return "释放一个车位成功";
    }


    /**
     * 闭锁：
     * 放假大爷要锁门：等待五个班的人全部走了才能上锁
     */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();  //等待锁门
        return "大爷锁门了";
    }


    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String lockDoor(@PathVariable("id") Long id) throws InterruptedException {
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown();  //计数减一
        return id + "班级走完了";
    }
}
